feat: surface unprocessable state deltas as fatal client errors - #6827
Conversation
When the backend sends a delta with a substate the frontend has no dispatch function for (mismatched frontend/backend state definitions), the frontend now: - validates the entire delta before dispatching anything, so a bad substate no longer partially applies an update or silently drops queued events, - logs an actionable error to the browser console, - reports the error to the backend via a new client_error socket event so it shows up in the terminal where devs look first, - treats the mismatch as fatal per reflex-dev#6019: no further events are sent until the frontend is rebuilt/reloaded, instead of erroring again on every interaction. Unexpected errors while applying a delta are likewise reported to the backend instead of vanishing as unhandled rejections. The backend on_client_error handler validates the payload shape, sanitizes and truncates client-supplied strings before logging, and only logs at error level for sockets with a linked token. Error type strings are shared via constants.ClientErrorType, and emit_update gained debug logging of outgoing substates (guarded by is_debug so the hot path is unaffected). Fixes reflex-dev#6019
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryAdds fatal frontend handling for unprocessable state deltas and reports sanitized, rate-limited client errors to backend exception handlers.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported logging-flood and cross-session suppression concerns are addressed by bounded reporting and visible suppression, while the partial-update concern was established as invalid.
|
| Filename | Overview |
|---|---|
| packages/reflex-base/src/reflex_base/.templates/web/utils/state.js | Validates incoming deltas before dispatch, reports processing failures, and makes missing-dispatch mismatches fatal until reload. |
| reflex/app.py | Adds a guarded client-error socket handler with sender validation, bounded logging, visible suppression, and exception-handler routing. |
| packages/reflex-base/src/reflex_base/utils/format.py | Adds bounded sanitization of client-provided values before they reach Rich-backed backend logging. |
| tests/integration/tests_playwright/test_client_error.py | Verifies end-to-end backend reporting and that an unprocessable delta stops subsequent client events until reload. |
| tests/units/test_app.py | Covers malformed reports, unknown sessions, sanitization, constant alignment, and both per-SID and process-wide rate limiting. |
Reviews (7): Last reviewed commit: "Update news/6827.feature.md" | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cfd1341892
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
All reported issues were addressed across 7 files
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
- Rate-limit error-level client_error logging to 5 entries per SID (cleared on disconnect) so a client that links an arbitrary token cannot flood backend logs. - Escape rich markup in sanitized client values; unescaped closing tags raised MarkupError and styling tags could inject into terminal logs. - Keep sanitized values within max_length including the truncation suffix. - Clear the event queue on fatal state mismatch; callers drain the queue in while-loops that would otherwise spin forever. - Await queueEvents inside the event handler try block so failures are reported via client_error instead of unhandled rejections; guard error.message for non-Error throws. - Add news fragments for the changelog check.
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Per-SID budgets reset when a new socket connects, so scripted reconnect loops could still flood backend logs. Add a process-wide time-window cap (20 entries per 60s) on top of the per-SID limit; later windows log again, so long-lived sessions are not silenced forever.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Fix all with cubic | Re-trigger cubic
masenf
left a comment
There was a problem hiding this comment.
if we're introducing a new client error message type on the transport, then we should wire up the existing frontend error handler to this mechanism.
- Wire on_client_error into app.frontend_exception_handler so custom handlers (e.g. error trackers) receive client-reported errors. - Reword the frontend mismatch message to suggest refreshing the page first, per review. - Remove the per-update substate debug log (too spammy for --loglevel debug). - Warn once per window when the client_error rate limit trips so suppression is never silent.
|
Wired the client_error mechanism into the existing frontend error handler in ba8beb3: |
A stale frontend build is the usual cause of a delta the frontend cannot dispatch, and a reload picks up the matching one. The reload is recorded in sessionStorage so a mismatch that survives it (e.g. api_url pointing at a different app) leaves the page up with the error reported rather than looping. Also from review: - Move _sanitize_client_log_value off EventNamespace into format.sanitize_client_log_value, slicing to max_length before the per-character scan so an oversized value costs no more than a bounded one. - Sanitize only after the SID and rate-limit checks, so reports that get dropped do not pay for it. - Escape rich markup in FrontendEventExceptionState, where a JS message containing square brackets could style backend logs or raise MarkupError. - Validate the delta in a single pass that allocates only on a miss, and stop awaiting queueEvents inside the event handler so a delta applies in full before a later update can interleave. - Fold tests/units/test_client_error.py into test_app.py and test_format.py, and cover the report and the reload in Playwright.
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
An automatic reload silently hides the failure this PR exists to surface, wipes page state to fix only the stale-build case, and in the wrong-api_url case produces a second identical report under a new SID that reads as a duplicate. Drop the reload and the sessionStorage once-per-tab guard: a mismatch now reports exactly once, stops further events, and leaves recovery (rebuild or api_url fix) to the developer.
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

Summary
When the backend sends a delta the frontend cannot process (no dispatch function registered for a substate), the app currently throws
dispatch[substate] is not a functionin the browser console and otherwise looks silently broken. This PR makes that failure loud, actionable, and visible in the backend terminal — where Python devs look first.Fixes #6019. Supersedes #6128, rebuilt on top of the
packages/reflex-baselayout with the review findings from that PR addressed.Frontend (
reflex_base/.templates/web/utils/state.js)update.events.client_errorsocket event.backend_state_mismatchflag stops all further event sending and drops incoming updates, so the error is reported once instead of on every interaction. A page reload (e.g. after rebuilding the frontend) resets it.Backend (
reflex/app.py)EventNamespace.on_client_errorhandler logs frontend-reported errors in the terminal with remediation steps (rebuild frontend / checkapi_url).emit_updategained debug logging of outgoing substates, guarded byconsole.is_debug()so the hot path doesn't pay for message construction.Shared constants
SocketEvent.CLIENT_ERRORand a newClientErrorTypenamespace inreflex_base.constants.eventkeep the error-type strings in one place, matched by theERROR_TYPE_*constants instate.js.Testing
tests/units/test_client_error.pycovers both error branches, malformed payloads (which previously raisedAttributeError), unknown-sid gating, and sanitization/truncation.tests/units/test_app.pyandtests/units/utils/test_token_manager.pypass alongside (164 passed).